package com.adjy.pathbuilders.world;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Random;

import org.jbox2d.dynamics.Body;

import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;

import com.adjy.pathbuilders.database.GameDatabase;
import com.adjy.pathbuilders.objects.Adrenalin;
import com.adjy.pathbuilders.objects.Apple;
import com.adjy.pathbuilders.objects.Arrow;
import com.adjy.pathbuilders.objects.Box;
import com.adjy.pathbuilders.objects.Cloud;
import com.adjy.pathbuilders.objects.Coin;
import com.adjy.pathbuilders.objects.Door;
import com.adjy.pathbuilders.objects.Drink;
import com.adjy.pathbuilders.objects.Fire;
import com.adjy.pathbuilders.objects.Floor;
import com.adjy.pathbuilders.objects.GameEntityType;
import com.adjy.pathbuilders.objects.Life;
import com.adjy.pathbuilders.objects.Thinkable;
import com.adjy.pathbuilders.objects.Wall;
import com.adjy.pathbuilders.physics.PhysicalWorld;
import com.adjy.pathbuilders.physics.QueryCollisionCallback;
import com.adjy.pathbuilders.rendering.GameCanvas;
import com.adjy.pathbuilders.rendering.loading.AnimationFactory;
import com.adjy.pathbuilders.scenario.Position;
import com.adjy.pathbuilders.scenario.ScenarioObject;
import com.adjy.pathbuilders.scenario.ScenarioObjectType;
import com.adjy.pathbuilders.screens.AnimationCoins;
import com.adjy.pathbuilders.screens.Counter;
import com.adjy.pathbuilders.screens.GameScreen;
import com.adjy.pathbuilders.screens.ScoreScreen;
import com.adjy.pathbuilders.screens.ShowdoorScreen;
import com.adjy.pathbuilders.sounds.SoundManager;
import com.adjy.pathbuilders.state.Game;
import com.adjy.pathbuilders.state.GameState;
import com.adjy.pathbuilders.touch.ObjectBox;
import com.adjy.pathbuilders.touch.TouchableObject;

/**
 * GameWorld class holds all information of the World of our Game<br>
 * This includes, GameObjects, Character, Scenarios, Physics, etc
 */
public class GameWorld implements GameScreen {

	private static final boolean DRAW_DEBUG = false;

	private PhysicalWorld pWorld;
	private AnimationFactory animationFactory;
	private Level level;
	private MainChar mainChar;
	private ArrayList<GameEntity> entities;
	private Random random;
	private Paint scorePaint;
	private Game game;
	private ObjectBox objectBox;
	private Queue<GameEntity> entitiesQueued;
	private ScoreScreen scorescreen;	
	private ShowdoorScreen sdoorScreen;
	private Counter selectableObjectsCounter;
	private AnimationCoins AC;
	public static float TEST_MAIN_CHAR_X = 1;
	public static float TEST_MAIN_CHAR_Y = 1;

	private int numberOfSelectedObjects;
	private WarningWindow selectedObjectsLimitWarning;

	private static final int TIME_OF_SELECTED_OBJECTS_WARNING = 10;
	private static final int LIMIT_OF_SELECTED_OBJECTS = 40;

	private float totalElapsedTime;

	public GameWorld(Game game, Level level, AnimationFactory anFactory) {
		this.game = game;
		this.level = level;
		this.animationFactory = anFactory;
		this.pWorld = new PhysicalWorld();
		this.entities = new ArrayList<GameEntity>();
		this.entitiesQueued = new LinkedList<GameEntity>();
		
		List<GameEntityType> boxEntities = new ArrayList<GameEntityType>();
		boxEntities.addAll(level.getSelectableObjects());
		this.objectBox = new ObjectBox(3, boxEntities,animationFactory, this);
		this.scorescreen = new ScoreScreen(anFactory, this);
		this.sdoorScreen = new ShowdoorScreen(anFactory,this);
		this.AC = new AnimationCoins(anFactory,this);
		this.selectedObjectsLimitWarning = new WarningWindow(anFactory,
				"Limit of selectable objects reached", 230, 20,
				TIME_OF_SELECTED_OBJECTS_WARNING);
		random = new Random();
		createPaint();
		totalElapsedTime = 0;
		numberOfSelectedObjects = 0;
		selectableObjectsCounter = new Counter(game, LIMIT_OF_SELECTED_OBJECTS, 0);
	}

	private void createPaint() {
		this.scorePaint = new Paint();
		this.scorePaint.setColor(Color.RED);
		this.scorePaint.setTextSize(15);
		this.scorePaint.setAntiAlias(true);
	}

	/**
	 * 
	 * @param floorPositions
	 * @param type
	 * @param position
	 * @return True, if the method added with success
	 */
	public boolean addGameEntity(ScenarioObjectType type, Position position) {
		if (type != null && position != null) {
			GameEntity objectToAdd = null;
			// FIXME a lot of duplication in this switch
			switch (type) {
			case MainCharacter:
				
				
				
				this.mainChar = new MainChar(position.getX(), position.getY(),
						animationFactory
								.getAnimationsFor(GameEntityType.MAINCHAR));
								
				objectToAdd = this.mainChar;
				break;

			case Floor:
				objectToAdd = new Floor(position.getX() - Floor.SIZE[0] / 2f,
						position.getY() + Floor.SIZE[1] / 2f,
						animationFactory.getAnimationsFor(GameEntityType.FLOOR));
				break;

			case Fire:
				objectToAdd = new Fire(position.getX() - Fire.SIZE[0] / 2f,
						position.getY() + Fire.SIZE[1] / 2f,
						animationFactory.getAnimationsFor(GameEntityType.FIRE));
				break;

			case Wall:
				objectToAdd = new Wall(position.getX() - Wall.SIZE[0] / 2f,
						position.getY() + Wall.SIZE[1] / 2f,
						animationFactory.getAnimationsFor(GameEntityType.WALL));
				break;

			case Apple:
				objectToAdd = new Apple(position.getX() - Apple.SIZE[0] / 2f,
						position.getY() + Apple.SIZE[1] / 2f,
						animationFactory.getAnimationsFor(GameEntityType.APPLE));
				break;

			case Box:
				objectToAdd = new Box(position.getX() - Box.SIZE[0] / 2f,
						position.getY() + Box.SIZE[1] / 2f,
						animationFactory.getAnimationsFor(GameEntityType.BOX));
				break;

			case BadItem:
				objectToAdd = createAsBadItem(position);
				break;

			case Adrenalin:
				objectToAdd = createAsAdrenalin(position);
				break;

			case Drink:
				objectToAdd = createAsDrink(position);
				break;

			case Door:
				float posXdoor = position.getX() - Arrow.SIZE[0]/2f - Door.SIZE[0] / 2f;
				float posYdoor = position.getY() + (Door.SIZE[1] / 2f)*5 + Arrow.SIZE[1]/2f;
				
				this.sdoorScreen.setPosXDoor((int)posXdoor);
				this.sdoorScreen.setPosYDoor((int)posYdoor);
				
				objectToAdd = new Door(
						position.getX() - Door.SIZE[0] / 2f,
						position.getY() + Door.SIZE[1] / 2f,
						animationFactory.getAnimationsFor(GameEntityType.DOOR), this.level);
				break;

			case Cloud:
				objectToAdd = new Cloud(position.getX() - Cloud.SIZE[0] / 2f,
						position.getY() + Cloud.SIZE[1] / 2f,
						animationFactory.getAnimationsFor(GameEntityType.CLOUD));
				break;

			case GoodItem:
				objectToAdd = createAsGoodItem(position);
				break;

			case Life:
				objectToAdd = createAsGoodItem(position,
						ScenarioObjectType.Life);
				break;

			case Coin:
				
				objectToAdd = createAsGoodItem(position,
						ScenarioObjectType.Coin);
				break;

			default:
				break;
			}

			if (objectToAdd != null) {
				pWorld.addObject(objectToAdd);

				if (!ScenarioObjectType.MainCharacter.equals(type))
					entities.add(objectToAdd);
				return true;
			}
		}

		return false;
	}

	/**
	 * Generates a random number between the min and max parameters.
	 * 
	 * @param min
	 * @param max
	 * @return
	 */
	private int random(int min, int max) {
		return (min + (int) ((max - min + 1) * Math.random()));
	}

	private GameEntity createAsBadItem(Position position) {
		ScenarioObjectType badItemType = badItemToAdd();
		if (badItemType == ScenarioObjectType.Adrenalin) {
			return createAsAdrenalin(position);
		} else if (badItemType == ScenarioObjectType.Drink) {
			return createAsDrink(position);
		} else {
			return null;
		}
	}

	private ScenarioObjectType badItemToAdd() {
		// FIXME verify the possible problems of use Math.abs
		int typeIndicator = Math.abs(random.nextInt() % 2);

		if (typeIndicator == 0) {
			return ScenarioObjectType.Drink;
		} else {
			return ScenarioObjectType.Adrenalin;
		}
	}

	private GameEntity createAsDrink(Position position) {
		return new Drink(position.getX(), position.getY(),
				animationFactory.getAnimationsFor(GameEntityType.DRINK));
	}

	private GameEntity createAsAdrenalin(Position position) {
		return new Adrenalin(position.getX(), position.getY(),
				animationFactory.getAnimationsFor(GameEntityType.ADRENALIN));
	}

	private GameEntity createAsGoodItem(Position position) {
		ScenarioObjectType goodItemType = goodItemToAdd();
		return createAsGoodItem(position, goodItemType);
	}

	private GameEntity createAsGoodItem(Position position,
			ScenarioObjectType goodItemType) {
		if (goodItemType == null)
			return null;
		switch (goodItemType) {
		case Life:
			return new Life(position.getX(), position.getY(),
					animationFactory.getAnimationsFor(GameEntityType.LIFE));

		case Coin:
			
			AC.createCoin(position.getX(),position.getY(),
					animationFactory.getAnimationsFor(GameEntityType.COIN));
			
			return new Coin(position.getX(), position.getY(),
					animationFactory.getAnimationsFor(GameEntityType.COIN));

		default:
			return null;
		}
	}

	private ScenarioObjectType goodItemToAdd() {
		int r = random(0, 2);
		switch (r) {
		case 0:
			return ScenarioObjectType.Life;

		case 1:
			return null;

		case 2:
			return ScenarioObjectType.Coin;

		default:
			return null;
		}
	}

	/**
	 * This method performs one step in the game world.<br>
	 * It performs (in this order): Input, Logic, Physics
	 * 
	 * @param timePassed
	 *            Elapsed Time in Seconds
	 */
	public void step(float timePassed) {

		// Add Entities Queued
		GameEntity entityToAdd;
		while ((entityToAdd = this.entitiesQueued.poll()) != null) {
			pWorld.addObject(entityToAdd);
			entities.add(entityToAdd);
		}

		this.objectBox.step(timePassed);

		// Do Any Logic here
		if (mainChar.isDead()) {
			this.game.changeState(GameState.GAME_OVER);
		} else if (mainChar.getCurrentStageProgress().equals(
				StageProgress.FINISHED)) {

			if (level.getNext() == null) {
				game.changeState(GameState.CLEARED);
			} else {
				game.changeState(GameState.FINISHED_LEVEL);
			}

			if (level.getBestTime() == -1
					|| level.getBestTime() > getCurrentTime()) {
				level.setBestTime((int) getCurrentTime());
			}

			if (level.getMostCoins() < mainChar.getPoints()) {
				level.setMostCoins(mainChar.getPoints());
			}

			// FIXME: Breaking encapsulation
			// For now, there's not a better way of doing this..
			GameDatabase.getInstance().setLevelInformation(level);
			if (level.getNext() != null)
				GameDatabase.getInstance().setLevelInformation(level.getNext());
			if (level.getNext() != null) {
				level.getNext().enable();
				GameDatabase.getInstance().setLevelInformation(level.getNext());
			}

		} else {
			mainChar.think(this, timePassed);
			for (int i = 0; i < entities.size(); i++) {
				GameEntity e = entities.get(i);
				if (e.isDead()) {
					entities.remove(e);
					pWorld.removeObject(e);
					i--;
					continue;
				}

				if (e instanceof Thinkable) {
					((Thinkable) e).think(this, timePassed);
				}
			}

			// Perform Physics
			pWorld.step(timePassed);

			totalElapsedTime += timePassed;
		}
	}

	/**
	 * Method to receive any input events coming on the screen
	 * 
	 * @param event
	 *            Event from the screen
	 */
	public void input(MotionEvent event) {
		this.objectBox.captureInput(event);
	}

	@Override
	public void render(GameCanvas canvas, float timeElapsed) {
		canvas.drawBackground(animationFactory.getImage("game_bg")); // FIXME:
																		// this
																		// should
																		// be
																		// static

		if (PhysicalWorld.USE_BOX_2D) {

			Body body = pWorld.getWorld().getBodyList();
			while (body != null) {
				if (body.m_userData == null) {
					body = body.getNext();
					continue;
				}

				GameEntity p = (GameEntity) body.m_userData;
				p.render(canvas, timeElapsed);

				body = body.getNext();

			}

		}
		if (DRAW_DEBUG)
			pWorld.render(canvas);

		// Above Everything
		this.objectBox.render(canvas, timeElapsed);
		this.scorescreen.render(canvas, timeElapsed);	
		this.sdoorScreen.render(canvas, timeElapsed);
		this.AC.render(canvas, timeElapsed);
		selectableObjectsCounter.render(canvas, timeElapsed);
		selectedObjectsLimitWarning.render(canvas, timeElapsed);
	}

	public boolean addTouchableObject(TouchableObject touchableObject) {
		GameEntity entity = touchableObject.createEntity(true);
		if (!checkCollisionFor(entity)) {
			if (++numberOfSelectedObjects <= LIMIT_OF_SELECTED_OBJECTS) {
				try {
					selectableObjectsCounter.incrementCurrent();
				} catch (Exception e) {
					e.printStackTrace();
				}
				entitiesQueued.add(entity);
				return true;
			} else {
				selectedObjectsLimitWarning.show();
			}
		}

		return false;
	}

	public boolean checkCollisionFor(GameEntity entity) {

		QueryCollisionCallback queryCallback = new QueryCollisionCallback();
		pWorld.getWorld().queryAABB(queryCallback, entity.getVirtualAABB());

		return queryCallback.hasCollisions();
	}

	public void start(GameCanvas mcanvas, float timeElapsed) {
		render(mcanvas, timeElapsed);
	}

	public float getCurrentTime() {
		return totalElapsedTime;
	}

	public MainChar getMainChar() {
		return mainChar;
	}

	@Override
	public void load() {
		List<Position> lockedPositions = new LinkedList<Position>();
		for (ScenarioObject o : this.level.getScenario().getScenarioObjects()) {
			// Creates a GameObject out of this ScenarioObject
			if (addGameEntity(o.getType(), o.position())) {
				lockedPositions.add(o.position());
			}
		}

		objectBox.load();
		scorescreen.load();		
		this.sdoorScreen.load();
		this.AC.load();
		SoundManager.getInstance().loadSoundEffects("adrenalin", "apple",
				"drink", "cloud", "death", "door", "coin", "wall");
	}

	@Override
	public void release() {
		objectBox.release();
		scorescreen.release();
		this.sdoorScreen.release();
		this.AC.release();
		SoundManager.getInstance().releaseSoundEffects("adrenalin", "apple",
				"drink", "cloud", "death", "door", "coin", "wall");
	}

	public void restart() {
		this.game.restartLevel();
	}

	public Level getLevel() {
		return level;
	}
}
